package applogic;
import java.rmi.server.ServerCloneException;
import java.sql.SQLException;
import java.text.DateFormat;
import java.util.*;
import java.util.Map.Entry;
public class CalenderClient implements Receiver
{
	private final int months[] = {31,28,31,30,31,30,31,31,30,31,30,31};
	private boolean[] loaded;
	public static CalenderClient client = null;
	public static ArrayList<User> users;
	public static Network connection;
	public static boolean waiting;
	GregorianCalendar time = new GregorianCalendar();
	private HashMap<String,HashMap<Integer,Appointment>> calendar;
	private boolean loggedOn;
	private boolean running;
	private ArrayList<Message> messages;
	private ArrayList<Message> newMessages;
	private Terminal terminal;
	private int payloadsExpected;
	private String payloadExpectedFor;
	private Appointment waitingForConfirmation;
	User user;

	public CalenderClient()
	{
		loaded = new boolean[53];
		user = null;
		messages = new ArrayList<Message>();
		newMessages = new ArrayList<Message>();
		loggedOn = false;
		running = true;
		calendar = new HashMap<String,HashMap<Integer,Appointment>>();
		payloadsExpected = 0;
		time.setTimeInMillis(System.currentTimeMillis());
	}

	public void login(String username, String password)
	{
		Message msg = new Message("login_request,"+username+","+password);
		msg.setTarget(0);
		connection.sendMessage(msg);
	}

	public boolean saveMeeting(Meeting meeting)
	{
		Message msg = new Message("meeting_storage_request,"+meeting.toMessage());
		msg.setTarget(0);
		connection.sendMessage(msg);
		waiting = true;
		waitingForConfirmation = meeting;
		return true;
	}
	
	public boolean editMeeting(Meeting meeting)
	{
		Message msg = new Message("meeting_storage_request,"+meeting.toMessage());
		msg.setTarget(0);
		connection.sendMessage(msg);
		return true;
	}

	public boolean saveAppointment(Appointment appointment)
	{
		Message msg = new Message("appointment_storage_request,"+appointment.toMessage());
		connection.sendMessage(msg);
		msg.setTarget(0);
		waiting = true;
		waitingForConfirmation = appointment;
		return true;
	}
	
	public boolean editAppointment(Appointment appointment)
	{
		Message msg = new Message("appointment_storage_request,"+appointment.toMessage());
		connection.sendMessage(msg);
		msg.setTarget(0);
		return true;
	}
	
	public void edit(int ID)
	{
		Appointment appointment = calendar.get(user.getUser()).get(ID);
		if(appointment==null||!appointment.getOwner().getUser().equals(user.getUser()))
		{
			terminal.display("Invalid appointment or you aren't the owner of the appointment or " +
					"appointment is not loaded from server (please make sure it's within the " +
					"time period you've requested from the server by using show dd.mm - dd.mm.)");
			return;
		}
		if(appointment instanceof Meeting)
			 createMeeting(ID);
		else createAppointment(ID);
	}

	public void createMeeting(int ID)
	{
		Meeting meeting = new Meeting();
		
		/* parse numbers from input until a valid input has been provided */
		int day,month;
		while(true)
		{
				try
			{
				String input = terminal.askForInput("Please enter day of meeting in format dd.mm (day.month):");
				StringTokenizer st = new StringTokenizer(input,".");
				day = Integer.parseInt(st.nextToken());
				month = Integer.parseInt(st.nextToken());
				
				/* check that the entered date is valid */
				if(month>12||month<1)throw new Exception("invalid month");
				if(day<1||day>months[month-1])throw new Exception("Invalid day "+day);
				break;
			}
			catch(Exception e)
			{
				System.out.println("Error in input: "+e.getMessage());
			}
		}
		meeting.setDate(day,month);
		
		/* parse numbers from input until a valid input has been provided */
		int start;
		while(true)
		{
				try
			{
				start = Integer.parseInt(terminal.askForInput("Please enter starting hour of meeting:"));
				
				/* check that the entered hour is valid */
				if(start<0||start>24)throw new Exception("Invalid hour "+start);
				break;
			}
			catch(Exception e)
			{
				System.out.println("Error in input: "+e.getMessage());
			}
		}
		meeting.setStart(start);
		
		/* parse numbers from input until a valid input has been provided */
		int end;
		while(true)
		{
				try
			{
				end = Integer.parseInt(terminal.askForInput("Please enter ending hour:"));
				
				/* check that the entered number of hours is reasonable and valid */
				if(end<0)throw new Exception("Invalid hour "+end);
				break;
			}
			catch(Exception e)
			{
				System.out.println("Error in input: "+e.getMessage());
			}
		}
		meeting.setEnd(end);
		
		/* ask for a description of the meeting */
		String input = terminal.askForInput("On the "+day+". of "+month+" you have registered a meeting from "+start+" to "+end
				+".\nPlease enter a description of the meeting (maximum 64 characters):");
		meeting.setDescription(input);
		
		/* ask user for list of attendees*/
		input = terminal.askForInput("Please enter usernames of internal attendees, separated by comma and/or space:");
		StringTokenizer st = new StringTokenizer(input,", ");
		while(st.hasMoreElements())meeting.addUser(new User(st.nextToken(),""));
		/*
		 * TODO: find and verify usernames and add them to meeting.
		 */
		
		int extAttendees;
		while(true)
		{
				try
			{
				extAttendees = Integer.parseInt(terminal.askForInput("Please enter number of external attendees:"));
				
				/* check that the entered number of hours is reasonable and valid */
				if(extAttendees<0)throw new Exception("Invalid number "+extAttendees);
				break;
			}
			catch(Exception e)
			{
				System.out.println("Error in input: "+e.getMessage());
			}
		}
		meeting.setExtAttendees(extAttendees);
		meeting.setStatus("waiting");
		if(ID==0)
		{
			System.out.println("Requesting room reservation from server...");
			meeting.setOwner(user);
			requestRoom(meeting);
		}
		else
		{
			meeting.setID(ID);
			meeting.setOwner(user);
			editMeeting(meeting);
			calendar.get(user.getUser()).put(ID, meeting);
			terminal.display("Successfully edited meeting.\n"+meeting);
		}
	}
	
	private static Meeting wMeeting;
	private void requestRoom(Meeting meeting) {
		waiting = true;
		wMeeting = meeting;
		Message msg = new Message("room_request,"+meeting.getNumOfAttendees()
									+","+meeting.getMonth()+","+meeting.getDay()
									+","+meeting.getStart()+","+meeting.getEnd());
		msg.setTarget(0);
		connection.sendMessage(msg);
	}

	public void createAppointment(int ID)
	{
		
		Appointment appointment = new Appointment();
		
		/* parse numbers from input until a valid input has been provided */
		
		int day,month;
		while(true)
		{
				try
			{
				String input = terminal.askForInput("Please enter day of appointment in format dd.mm (day.month):");
				StringTokenizer st = new StringTokenizer(input,".");
				day = Integer.parseInt(st.nextToken());
				month = Integer.parseInt(st.nextToken());
				
				/* check that the entered date is valid */
				if(month>12||month<1)throw new Exception("invalid month");
				if(day<1||day>months[month-1])throw new Exception("Invalid day "+day);
				break;
			}
			catch(Exception e)
			{
				System.out.println("Error in input: "+e.getMessage());
			}
		}
		appointment.setDate(day,month);
		
		/* parse numbers from input until a valid input has been provided */
		int start;
		while(true)
		{
				try
			{
				start = Integer.parseInt(terminal.askForInput("Please enter starting hour of appointment:"));
				
				/* check that the entered hour is valid */
				if(start<0||start>24)throw new Exception("Invalid hour "+start);
				break;
			}
			catch(Exception e)
			{
				System.out.println("Error in input: "+e.getMessage());
			}
		}
		appointment.setStart(start);
		
		/* parse numbers from input until a valid input has been provided */
		int end;
		while(true)
		{
				try
			{
				end = Integer.parseInt(terminal.askForInput("Please enter ending hour of appointment:"));
				
				/* check that the entered number of hours is reasonable and valid */
				if(end<0)throw new Exception("Invalid duration "+end);
				break;
			}
			catch(Exception e)
			{
				System.out.println("Error in input: "+e.getMessage());
			}
		}
		appointment.setEnd(end);
		
		/* ask for a description of the appointment */
		String input = terminal.askForInput("On the "+day+". of "+month+" you have registered an appointment from "+start+" to "+end
				+".\nPlease enter a description of the appointment (maximum 64 characters):");
		appointment.setDescription(input);
		
		/* ask user for a location for the appointment */
		input = terminal.askForInput("Please describe location of the appointment (maximum 64 characters");
		appointment.setPlace(input);
		
		if(ID==0)
		{	
			appointment.setOwner(user);
			saveAppointment(appointment);
		}
		else
		{
			appointment.setID(ID);
			appointment.setOwner(user);
			terminal.display("Successfully edited appointment:\n"+appointment);
			editAppointment(appointment);
			calendar.get(user.getUser()).put(ID,appointment);
		}
	}

	public void receiveMessage(Message message)
	{
		newMessages.add(message);
	}
	
	public void parseMessage(Message message)
	{
		String content = message.getContent();
		MsgType type = message.getType();
		switch(type)
		{
		case DECLINED_NOTICE:
		case DROPPED_NOTICE:
		case MEETING_NOTICE:
		case BROKEN:
			messages.add(message);
			System.out.println("You have a new message!");
			break;
		case ROOM_REQUEST:
			break;
		case ROOM_ALLOCATION:
			wMeeting.setRoom(Integer.parseInt(content.substring(1)));
			saveMeeting(wMeeting);
			terminal.display("You have been allocated room "+wMeeting.getRoom());
			waiting = false;
			break;
		case LOGIN_REQUEST:
			break;
		case LOGIN_RESULT:
			if(content.equals(",granted"))
			{
				loggedOn=true;
				terminal.display("Successfully logged on! Type help to view available commands.");
				calendar.put(user.getUser(), new HashMap<Integer,Appointment>());
			}
			else
			{
				user = null;
				System.out.println("Server refused login, please retry:");
				String username = terminal.askForInput("Please enter username:").toLowerCase();
				String password = terminal.askForInput("Please enter password:").toLowerCase();
				user = new User(username,password);
				login(username,password);
			}
			break;
		case MEETING_STORAGE_REQUEST:
			break;
		case APPOINTMENT_STORAGE_REQUEST:
			break;
		case STORAGE_CONFIRMATION:
			break;
		case RSVP:
			user.addRsvp(new Rsvp(content));
			break;
		case CALENDER_REQUEST:
			break;
		case CALENDER_REQUEST_RESPONSE:
			payloadsExpected+=Integer.parseInt(content.substring(1));
			if(payloadsExpected==0)
			{
				terminal.display("No appointments registered for that user in the given week.");
				waiting = false;
			}
			break;
		case CALENDER_PAYLOAD:
		{
			StringTokenizer st = new StringTokenizer(content,",");
			String t = st.nextToken();
			Appointment appointment;
			
			if(t.equals("appointment"))
			{
				appointment = new Appointment();
			}
			else {
				appointment = new Meeting();
			}
			
			appointment.buildFromNetMsg(st.nextToken(""));
			
			HashMap<Integer,Appointment> map;
			map = calendar.get(appointment.getOwner().getUser());
			
			if(map==null)
			{
				map = new HashMap<Integer,Appointment>();
				calendar.put(appointment.getOwner().getUser(), map);
			}
			
			map.put(appointment.getID(), appointment);
			
			if(appointment instanceof Meeting)
			{
				for(User user : ((Meeting) appointment).getAttendees())
				{
					map = calendar.get(user.getUser());
					
					if(map==null)
					{
						map = new HashMap<Integer,Appointment>();
						calendar.put(user.getUser(), map);
					}
					
					map.put(appointment.getID(), appointment);
				}
			}
			
			payloadsExpected--;
			if(payloadsExpected==0)
			{
				waiting = false;
				loaded[time.get(Calendar.WEEK_OF_YEAR)+weekMod]=true;
				showAppointments(payloadExpectedFor);
			}
		}//END CALENDER_PAYLOAD
			break;
		case MEETING_UPDATE:
		{
			Meeting meeting = new Meeting();
			meeting.buildFromNetMsg(content);
			
			
			HashMap<Integer,Appointment> map;
			map = calendar.get(meeting.getOwner().getUser());
			
			if(map==null)
			{
				map = new HashMap<Integer,Appointment>();
				calendar.put(meeting.getOwner().getUser(), map);
			}
			else
			{
				/*
				 * remove any old entries of this meeting
				 */
				Meeting oldMeeting = (Meeting)map.get(meeting.getID());
				if(oldMeeting!=null)
				{
					map.remove(meeting.getID());
					
					for(User user : oldMeeting.getAttendees())
					{
						HashMap<Integer,Appointment> usermap = calendar.get(user.getUser());
						if(usermap!=null)usermap.remove(meeting.getID());
					}
				}
			}
			
			map.put(meeting.getID(), meeting);
			
			for(User user : meeting.getAttendees())
			{
				map = calendar.get(user.getUser());
				
				if(map==null)
				{
					map = new HashMap<Integer,Appointment>();
					calendar.put(user.getUser(), map);
				}
				
				map.put(meeting.getID(), meeting);
			}
			
			System.out.println("Meeting "+meeting.getID()+" has received an update");
			
		}//END MEETING_UPDATE
			break;
		case STORE_CONFIRMATION:
			waitingForConfirmation.setID(Integer.parseInt(content.substring(1)));
			terminal.display("Successfully constructed "+(waitingForConfirmation instanceof Meeting?"meeting":"appointment")+":\n"+waitingForConfirmation);
			calendar.get(user.getUser()).put(waitingForConfirmation.getID(),waitingForConfirmation);
			waiting = false;
			break;
		}
		
	}
	
	private int weekMod = 0;
	
	public void modWeekMod(int mod)
	{
		if(time.get(Calendar.WEEK_OF_YEAR)+weekMod+mod<54
				&& time.get(Calendar.WEEK_OF_YEAR)+weekMod+mod>=0)weekMod+=mod;
		terminal.display("Week set to "+(time.get(Calendar.WEEK_OF_YEAR)+weekMod));
	}
	
	public void setWeek(int week)
	{
		if(week<0||week>53){
			terminal.display("Invalid week");
			return;
		}
		weekMod = week - time.get(Calendar.WEEK_OF_YEAR);
		terminal.display("Week set to "+(time.get(Calendar.WEEK_OF_YEAR)+weekMod));
	}
	
	public boolean inWeek(Appointment arg)
	{
		GregorianCalendar t = new GregorianCalendar();
		t.set(time.get(Calendar.YEAR), arg.getMonth()-1, arg.getDay());
		t.getTimeInMillis(); //"useless" operation to force calendar class to calculate this field.
		return t.get(Calendar.WEEK_OF_YEAR)==time.get(Calendar.WEEK_OF_YEAR)+weekMod;
	}
	public void showAppointments(String username)
	{
		if(!loaded[time.get(Calendar.WEEK_OF_YEAR)+weekMod])
		{
			requestAppointments(time.get(Calendar.WEEK_OF_YEAR)+weekMod);
			payloadExpectedFor = username;
			waiting = true;
			return;
		}
		HashMap<Integer,Appointment> map = calendar.get(username);
		
		if(map==null)
		{
			terminal.display("Invalid user");
			return;
		}
		
		if(map.size()==0)
		{
			terminal.display("No appointments registered for that user\n"+map);
			return;
		}
		
		Appointment[] array = new Appointment[64];
		Iterator<Entry<Integer,Appointment>> i = map.entrySet().iterator();
		
		//add with insertion sorting
		int count = 0;
		while(i.hasNext())
		{
			int index = count;
			Appointment tmp = i.next().getValue();
			
			if(!inWeek(tmp))continue;
			
			array[count]=tmp;
			
			while(index>0&&array[index-1].getMonth()>array[index].getMonth())
			{
				array[index]=array[index-1];
				index--;
				array[index]=tmp;
			}
			
			while(index>0&&array[index-1].getDay()>array[index].getDay())
			{
				array[index]=array[index-1];
				index--;
				array[index]=tmp;
			}
			
			while(index>0&&array[index-1].getStart()>array[index].getStart())
			{
				array[index]=array[index-1];
				index--;
				array[index]=tmp;
			}
			
			count++;
		}
		
		System.out.println("On this week "+username+" has the following appointments / meetings:");
		for(int a=0;a<count;a++)
		{
			System.out.println("\n"+(array[a] instanceof Meeting ? "Meeting:":"Appointment")+"\n"+array[a]);
		}
	}

	private void requestAppointments(int week) {
		Message message = new Message("calender_request,"+user+","+week);
		connection.sendMessage(message);
	}

	public boolean isRunning()
	{
		return running;
	}

	public void exit()
	{
		running = false;
	}

	public static void main(String[]args) throws InterruptedException
	{
		try{
		/*
		 * initializations
		 */
		client = new CalenderClient();
		users = new ArrayList<User>();
		Terminal terminal = new Terminal(client);
		client.terminal = terminal;
		Scanner sc = new Scanner(System.in);
		connection = new Network(client);
		Message.initialize();
		waiting = false;
		
		/*
		 * TODO: REMOVE DEBUG
		 */
		CalenderServer.initialize(connection);
		/*
		 * login procedures
		 */
		String username="";
		while(username.length()==0)username = terminal.askForInput("Please enter username:").toLowerCase();
		String password="";
		while(password.length()==0)password = terminal.askForInput("Please enter password:").toLowerCase();
		client.user = new User(username,password);
		client.login(username,password);
		/*
		 * TODO: communicate with server to verify login
		 */
		
		while(client.isRunning())
		{
			/*
			 * TODO: REMOVE DEBUG
			 */
			CalenderServer.mainloop();
			
			
			if(client.newMessages.size()>0)
			{
				/*
				 * Put the messages to be iterated in a holder
				 * to avoid concurrent modification should a new
				 * message be received during processing.
				 */
				ArrayList<Message> holder = client.newMessages;
				client.newMessages = new ArrayList<Message>();
				for(Message msg: holder)
				{
					client.parseMessage(msg);
				}
				continue;
			}
			if(client.loggedOn&&!waiting)
			{
				terminal.parseCommand(sc.nextLine());
			}
			else
			{
				/*
				 * Thread should not be overactive while waiting
				 */
				Thread.sleep(50);
			}
			
		}
		}catch(Exception e)
		{
			e.printStackTrace();
			client.exit();
			System.out.println("Doomsday alert!");
			System.exit(-1);
		}
	}

	public void delete(int ID) {
		Appointment app = calendar.get(user.getUser()).get(ID);
		if(app==null)
		{
			terminal.display("Invalid appointment or you aren't the owner of the appointment or " +
					"appointment is not loaded from server (please make sure it's within the " +
					"time period you've requested from the server by using show "+user+")");
			return;
		}
		if(app.getOwner().getUser().equals(user.getUser()))
		{
			connection.sendMessage(new Message("delete_request,"+ID));
		}
		else
		{
			String description = terminal.askForInput("Describe why you wish to cancel the meeting?");
			connection.sendMessage(new Message(new Rsvp(ID,user,-2,description).toMessage()));
		}
		calendar.get(user.getUser()).remove(ID);
		terminal.display("Deleted appointment.");
	}

	public void showMessages() {
		ArrayList<Message> holder = messages;
		messages = new ArrayList<Message>();
		Iterator<Message> i = holder.iterator();
		while(i.hasNext())
		{
			Message msg = i.next();
			switch(msg.getType())
			{
			case BROKEN:
				System.out.println("The following broken transmission has arrived:\n"+msg.getContent());
				break;
			case MEETING_NOTICE:
				{
					Meeting meeting = new Meeting();
					meeting.buildFromNetMsg(msg.getContent());
					if(!(meeting.getCanceled()==1))
					{
						System.out.println("You have been called in to attend a meeting:\n"+meeting);
						String response = terminal.askForInput("Will you attend? Yes/No/Wait (defaults to wait)").toLowerCase();
						
						Rsvp rsvp = new Rsvp(meeting.getID(), user, 0, "");
						if(response.equals("yes"))rsvp.setRsvp(1);
						if(response.equals("no"))rsvp.setRsvp(-1);
						connection.sendMessage(new Message(rsvp.toMessage()));
					}
					else
					{
						System.out.println("A meeting you were called in to has been canceled:\n"+meeting);
						connection.sendMessage(new Message("cancel_confirmation,"+user+","+meeting.getID()));
					}
				}
				break;
			case DROPPED_NOTICE:
				{
					System.out.println("You have been dropped from a meeting:\n");
					Meeting meeting = new Meeting();
					meeting.buildFromNetMsg(msg.getContent());
					System.out.println(meeting);
					Message answer = new Message("dropped_confirmation,"+user+","+meeting.getID());
					answer.setTarget(0);
					connection.sendMessage(answer);
				}
				break;
			case DECLINED_NOTICE:
				{
					System.out.print("The following meeting has been declined by users  ");
					StringTokenizer st = new StringTokenizer(msg.getContent(),",");
					int count = Integer.parseInt(st.nextToken());
					String decliners = "";
					for(;count>0;count--)decliners+=","+st.nextToken();
					System.out.println(decliners.substring(1));
					Meeting meeting = new Meeting();
					meeting.buildFromNetMsg(st.nextToken(""));
					System.out.println(meeting);
					String input="";
					calendar.get(user.getUser()).put(meeting.getID(),meeting);
					while(!input.toLowerCase().matches("drop|edit"))input = terminal.askForInput("\n\nDo you wish to drop these users from the meeting or edit the meeting? drop/edit");
					if(input.equals("edit"))edit(meeting.getID());
					else connection.sendMessage(new Message("drop_users,"+meeting.getID()+decliners));
					
				}	
				break;
			}
		}
	}
}